/*
 * jQuery Plugin: Tokenizing Autocomplete Text Entry
 * Version 1.6.0
 *
 * Copyright (c) 2009 James Smith (http://loopj.com)
 * Licensed jointly under the GPL and MIT licenses,
 * choose which one suits your project best!
 *
 */

(function ($) {
// Default settings
var DEFAULT_SETTINGS = {
	// Search settings
   method: "GET",
   contentType: "json",
   queryParam: "q",
   searchDelay: 300,
   minChars: 1,
   propertyToSearch: "name",
   jsonContainer: null,

	// Display settings
   hintText: "Type in a search term",
   noResultsText: "No results",
   searchingText: "Searching...",
   deleteText: "&times;",
   animateDropdown: true,

	// Tokenization settings
   tokenLimit: null,
   tokenDelimiter: ",",
   preventDuplicates: false,

	// Output settings
   tokenValue: "id",

	// Prepopulation settings
   prePopulate: null,
   processPrePopulate: false,

	// Manipulation settings
   idPrefix: "token-input-",

	// Formatters
   resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
   tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },

	// Callbacks
   onResult: null,
   onAdd: null,
   onBlankAdd: null,
   onDelete: null,
   onReady: null
};

// Default classes to use when theming
var DEFAULT_CLASSES = {
   tokenList: "token-input-list",
   token: "token-input-token",
   tokenDelete: "token-input-delete-token",
   selectedToken: "token-input-selected-token",
   highlightedToken: "token-input-highlighted-token",
   dropdown: "token-input-dropdown",
   dropdownItem: "token-input-dropdown-item",
   dropdownItem2: "token-input-dropdown-item2",
   selectedDropdownItem: "token-input-selected-dropdown-item",
   focused: "token-input-focused",
   inputToken: "token-input-input-token"
};

// Input box position "enum"
var POSITION = {
   BEFORE: 0,
   AFTER: 1,
   END: 2
};

// Keys "enum"
var KEY = {
   BACKSPACE: 8,
   TAB: 9,
   ENTER: 13,
   ESCAPE: 27,
   SPACE: 32,
   PAGE_UP: 33,
   PAGE_DOWN: 34,
   END: 35,
   HOME: 36,
   LEFT: 37,
   UP: 38,
   RIGHT: 39,
   DOWN: 40,
   NUMPAD_ENTER: 108,
   COMMA: 188
};

// Additional public (exposed) methods
var methods = {
   init: function(url_or_data_or_function, options) {
      var settings = $.extend({}, DEFAULT_SETTINGS, options || {});

      return this.each(function () {
         $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
      });
   },
   clear: function() {
      this.data("tokenInputObject").clear();
      return this;
   },
   add: function(item) {
      this.data("tokenInputObject").add(item);
      return this;
   },
   remove: function(item) {
      this.data("tokenInputObject").remove(item);
      return this;
   },
   get: function() {
   	return this.data("tokenInputObject").getTokens();
   }
}

// Expose the .tokenInput function to jQuery as a plugin
$.fn.tokenInput = function (method) {
   // Method calling and initialization logic
   if(methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
   } else {
      return methods.init.apply(this, arguments);
   }
};

// TokenList class for each input
$.TokenList = function (input, url_or_data, settings) {
   //
   // Initialization
   //
   
   // VANILLA
   $(input).bind('BeforeSubmit', function(e, frm){
     var val = $(input_box).val();
     if (val.length)
       add_blank_token(val);
   });

   // Configure the data source
   if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
      // Set the url to query against
      settings.url = url_or_data;

      // If the URL is a function, evaluate it here to do our initalization work
      var url = computeURL();

      // Make a smart guess about cross-domain if it wasn't explicitly specified
      if(settings.crossDomain === undefined) {
         if(url.indexOf("://") === -1) {
            settings.crossDomain = false;
         } else {
            settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
         }
      }
   } else if(typeof(url_or_data) === "object") {
      // Set the local data to search through
      settings.local_data = url_or_data;
   }

   // Build class names
   if(settings.classes) {
      // Use custom class names
      settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
   } else if(settings.theme) {
      // Use theme-suffixed default class names
      settings.classes = {};
      $.each(DEFAULT_CLASSES, function(key, value) {
         settings.classes[key] = value + "-" + settings.theme;
      });
   } else {
      settings.classes = DEFAULT_CLASSES;
   }
   
   // VANILLA
   var cancel_request = false;

   // Save the tokens
   var saved_tokens = [];

   // Keep track of the number of tokens in the list
   var token_count = 0;

   // Basic cache to save on db hits
   var cache = new $.TokenList.Cache();

   // Keep track of the timeout, old vals
   var timeout;
   var input_val;

   // Create a new text input an attach keyup events
   var input_box = $("<input type=\"text\"  autocomplete=\"off\">")
      .css({
         outline: "none"
      })
      .attr("id", settings.idPrefix + input.id)
      .focus(function () {
         if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
            show_dropdown_hint();
         }
         token_list.addClass(settings.classes.focused);
      })
      .blur(function () {
         hide_dropdown();
         $(this).val("");
         token_list.removeClass(settings.classes.focused);
      })
      .bind("keyup keydown blur update", resize_input)
      .keydown(function (event) {
         var previous_token;
         var next_token;

         switch(event.keyCode) {
            case KEY.LEFT:
            case KEY.RIGHT:
            case KEY.UP:
            case KEY.DOWN:
               if(!$(this).val()) {
                  previous_token = input_token.prev();
                  next_token = input_token.next();

                  if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
                     // Check if there is a previous/next token and it is selected
                     if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
                        deselect_token($(selected_token), POSITION.BEFORE);
                     } else {
                        deselect_token($(selected_token), POSITION.AFTER);
                     }
                  } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
                     // We are moving left, select the previous token if it exists
                     select_token($(previous_token.get(0)));
                  } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
                     // We are moving right, select the next token if it exists
                     select_token($(next_token.get(0)));
                  }
               } else {
                  var dropdown_item = null;

                  if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
                     dropdown_item = $(selected_dropdown_item).next();
                  } else {
                     dropdown_item = $(selected_dropdown_item).prev();
                  }

                  if(dropdown_item.length) {
                     select_dropdown_item(dropdown_item);
                  }
                  return false;
               }
               break;

            case KEY.BACKSPACE:
               previous_token = input_token.prev();

               if(!$(this).val().length) {
                  if(selected_token) {
                     delete_token($(selected_token));
                     hidden_input.change();
                  } else if(previous_token.length) {
                     select_token($(previous_token.get(0)));
                  }

                  return false;
               } else if($(this).val().length === 1) {
                  hide_dropdown();
               } else {
                  // set a timeout just long enough to let this function finish.
                  setTimeout(function(){do_search();}, 5);
               }
               break;

            case KEY.TAB:
            case KEY.ENTER:
            case KEY.NUMPAD_ENTER:
            case KEY.COMMA:
               if(selected_dropdown_item) {
                  add_token($(selected_dropdown_item).data("tokeninput"));
                  hidden_input.change();
                  return false;
               }
               // VANILLA
               else {
                  var val = $(input_box).val();
                  add_blank_token(val);
                  cancel_request = true;
                  return false;
               }
               break;

            case KEY.ESCAPE:
               hide_dropdown();
               return true;

            default:
               if(String.fromCharCode(event.which)) {
                  // set a timeout just long enough to let this function finish.
                  setTimeout(function(){do_search();}, 5);
               }
               break;
         }
      });

   // Keep a reference to the original input box
   var hidden_input = $(input)
                     .hide()
                     .val("")
                     .focus(function () {
                        input_box.focus();
                     })
                     .blur(function () {
                        input_box.blur();
                     });

   // Keep a reference to the selected token and dropdown item
   var selected_token = null;
   var selected_token_index = 0;
   var selected_dropdown_item = null;

   // The list to store the token items in
   var token_list = $("<ul />")
      .addClass(settings.classes.tokenList)
      .click(function (event) {
         var li = $(event.target).closest("li");
         if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
            toggle_select_token(li);
         } else {
            // Deselect selected token
            if(selected_token) {
               deselect_token($(selected_token), POSITION.END);
            }

            // Focus input box
            input_box.focus();
         }
      })
      .mouseover(function (event) {
         var li = $(event.target).closest("li");
         if(li && selected_token !== this) {
            li.addClass(settings.classes.highlightedToken);
         }
      })
      .mouseout(function (event) {
         var li = $(event.target).closest("li");
         if(li && selected_token !== this) {
            li.removeClass(settings.classes.highlightedToken);
         }
      })
      .insertBefore(hidden_input);

   // The token holding the input box
   var input_token = $("<li />")
      .addClass(settings.classes.inputToken)
      .appendTo(token_list)
      .append(input_box);

   // The list to store the dropdown items in
   var dropdown = $("<div>")
      .addClass(settings.classes.dropdown)
      .appendTo("body")
      .hide();

   // Magic element to help us resize the text input
   var input_resizer = $("<tester/>")
      .insertAfter(input_box)
      .css({
         position: "absolute",
         top: -9999,
         left: -9999,
         width: "auto",
         fontSize: input_box.css("fontSize"),
         fontFamily: input_box.css("fontFamily"),
         fontWeight: input_box.css("fontWeight"),
         letterSpacing: input_box.css("letterSpacing"),
         whiteSpace: "nowrap"
      });

   // Pre-populate list if items exist
   hidden_input.val("");
   var li_data = settings.prePopulate || hidden_input.data("pre");
   if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
      li_data = settings.onResult.call(hidden_input, li_data);
   }
   if(li_data && li_data.length) {
      $.each(li_data, function (index, value) {
         insert_token(value);
         checkTokenLimit();
      });
   }

   // Initialization is done
   if($.isFunction(settings.onReady)) {
      settings.onReady.call();
   }

   //
   // Public functions
   //

   this.clear = function() {
      token_list.children("li").each(function() {
         if ($(this).children("input").length === 0) {
            delete_token($(this));
         }
      });
   }

   this.add = function(item) {
      add_token(item);
   }

   this.remove = function(item) {
      token_list.children("li").each(function() {
         if ($(this).children("input").length === 0) {
            var currToken = $(this).data("tokeninput");
            var match = true;
            for (var prop in item) {
               if (item[prop] !== currToken[prop]) {
                  match = false;
                  break;
               }
            }
            if (match) {
               delete_token($(this));
            }
         }
      });
   }
   
   this.getTokens = function() {
   		return saved_tokens;
   	}

   //
   // Private functions
   //

   function checkTokenLimit() {
      if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
         input_box.hide();
         hide_dropdown();
         return;
      }
   }

   function resize_input() {
      if(input_val === (input_val = input_box.val())) {return;}

      // Enter new content into resizer and resize input accordingly
      var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      input_resizer.html(escaped);
      input_box.width(input_resizer.width() + 30);
   }

   function is_printable_character(keycode) {
      return ((keycode >= 48 && keycode <= 90) ||    // 0-1a-z
            (keycode >= 96 && keycode <= 111) ||   // numpad 0-9 + - / * .
            (keycode >= 186 && keycode <= 192) ||   // ; = , - . / ^
            (keycode >= 219 && keycode <= 222));   // ( \ ) '
   }

   // Inner function to a token to the list
   function insert_token(item) {
      // VANILLA: convert string to token
      if($.type(item) === "string")
        item = {"id": item, "name": item};
      
      var this_token = settings.tokenFormatter(item);
      this_token = $(this_token)
        .addClass(settings.classes.token)
        .insertBefore(input_token);

      // The 'delete token' button
      $("<span>" + settings.deleteText + "</span>")
         .addClass(settings.classes.tokenDelete)
         .appendTo(this_token)
         .click(function () {
            delete_token($(this).parent());
            hidden_input.change();
            return false;
         });

      // Store data on the token
      var token_data = {"id": item.id};
      token_data[settings.propertyToSearch] = item[settings.propertyToSearch];
      $.data(this_token.get(0), "tokeninput", item);

      // Save this token for duplicate checking
      saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
      selected_token_index++;

      // Update the hidden input
      update_hidden_input(saved_tokens, hidden_input);

      token_count += 1;

      // Check the token limit
      if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
         input_box.hide();
         hide_dropdown();
      }

      return this_token;
   }
   
   // VANILLA
   // Add ability to create tokens on the fly
   function add_blank_token(item) {
      var callback = settings.onBlankAdd;
      
      // Build our token item from scratch
      if (item.trim() == '')
         return;
            
      // Insert the new tokens
      if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
         insert_token(item);
         checkTokenLimit();
      }
      
      // Clear input box
      input_box.val("").focus();
      
      // Don't show the help dropdown, they've got the idea
      hide_dropdown();
      
      // Execute the onBlankAdd callback if defined
      if($.isFunction(callback)) {
         callback.call('a', hidden_input,item);
      }
   }

   // Add an existing token to the token list
   function add_token (item) {
      var callback = settings.onAdd;

      // See if the token already exists and select it if we don't want duplicates
      if(token_count > 0 && settings.preventDuplicates) {
         var found_existing_token = null;
         token_list.children().each(function () {
            var existing_token = $(this);
            var existing_data = $.data(existing_token.get(0), "tokeninput");
            if(existing_data && existing_data.id === item.id) {
               found_existing_token = existing_token;
               return false;
            }
         });

         if(found_existing_token) {
            select_token(found_existing_token);
            input_token.insertAfter(found_existing_token);
            input_box.focus();
            return;
         }
      }

      // Insert the new tokens
      if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
         insert_token(item);
         checkTokenLimit();
      }

      // Clear input box
      input_box.val("");

      // Don't show the help dropdown, they've got the idea
      hide_dropdown();

      // Execute the onAdd callback if defined
      if($.isFunction(callback)) {
         callback.call(hidden_input,item);
      }
   }

   // Select a token in the token list
   function select_token (token) {
      token.addClass(settings.classes.selectedToken);
      selected_token = token.get(0);

      // Hide input box
      input_box.val("");

      // Hide dropdown if it is visible (eg if we clicked to select token)
      hide_dropdown();
   }

   // Deselect a token in the token list
   function deselect_token (token, position) {
      token.removeClass(settings.classes.selectedToken);
      selected_token = null;

      if(position === POSITION.BEFORE) {
         input_token.insertBefore(token);
         selected_token_index--;
      } else if(position === POSITION.AFTER) {
         input_token.insertAfter(token);
         selected_token_index++;
      } else {
         input_token.appendTo(token_list);
         selected_token_index = token_count;
      }

      // Show the input box and give it focus again
      input_box.focus();
   }

   // Toggle selection of a token in the token list
   function toggle_select_token(token) {
      var previous_selected_token = selected_token;

      if(selected_token) {
         deselect_token($(selected_token), POSITION.END);
      }

      if(previous_selected_token === token.get(0)) {
         deselect_token(token, POSITION.END);
      } else {
         select_token(token);
      }
   }

   // Delete a token from the token list
   function delete_token (token) {
      // Remove the id from the saved list
      var token_data = $.data(token.get(0), "tokeninput");
      var callback = settings.onDelete;

      var index = token.prevAll().length;
      if(index > selected_token_index) index--;

      // Delete the token
      token.remove();
      selected_token = null;

      // Show the input box and give it focus again
      input_box.focus();

      // Remove this token from the saved list
      saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
      if(index < selected_token_index) selected_token_index--;

      // Update the hidden input
      update_hidden_input(saved_tokens, hidden_input);

      token_count -= 1;

      if(settings.tokenLimit !== null) {
         input_box
            .show()
            .val("")
            .focus();
      }

      // Execute the onDelete callback if defined
      if($.isFunction(callback)) {
         callback.call(hidden_input,token_data);
      }
   }

   // Update the hidden input box value
   function update_hidden_input(saved_tokens, hidden_input) {
      var token_values = $.map(saved_tokens, function (el) {
         return el[settings.tokenValue];
      });
      hidden_input.val(token_values.join(settings.tokenDelimiter));

   }

   // Hide and clear the results dropdown
   function hide_dropdown () {
      dropdown.hide().empty();
      selected_dropdown_item = null;
   }

   function show_dropdown() {
      dropdown
         .css({
            position: "absolute",
            top: $(token_list).offset().top + $(token_list).outerHeight(),
            left: $(token_list).offset().left,
            zindex: 999
         })
         .show();
   }

   function show_dropdown_searching () {
      if(settings.searchingText) {
         dropdown.html("<p>"+settings.searchingText+"</p>");
         show_dropdown();
      }
   }

   function show_dropdown_hint () {
      if(settings.hintText) {
         dropdown.html("<p>"+settings.hintText+"</p>");
         show_dropdown();
      }
   }

   // Highlight the query part of the search term
   function highlight_term(value, term) {
      return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
   }
   
   function find_value_and_highlight_term(template, value, term) {
      return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
   }

   // Populate the results dropdown with some results
   function populate_dropdown (query, results) {
      if(results && results.length && !cancel_request) { // VANILLA: && !cancel_request
         dropdown.empty();
         var dropdown_ul = $("<ul>")
            .appendTo(dropdown)
            .mouseover(function (event) {
               select_dropdown_item($(event.target).closest("li"));
            })
            .mousedown(function (event) {
               add_token($(event.target).closest("li").data("tokeninput"));
               hidden_input.change();
               return false;
            })
            .hide();

         $.each(results, function(index, value) {
            var this_li = settings.resultsFormatter(value);
            
            this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);         
            
            this_li = $(this_li).appendTo(dropdown_ul);
            
            if(index % 2) {
               this_li.addClass(settings.classes.dropdownItem);
            } else {
               this_li.addClass(settings.classes.dropdownItem2);
            }

            if(index === 0) {
               select_dropdown_item(this_li);
            }

            $.data(this_li.get(0), "tokeninput", value);
         });

         show_dropdown();

         if(settings.animateDropdown) {
            dropdown_ul.slideDown("fast");
         } else {
            dropdown_ul.show();
         }
      } else if (!cancel_request) { // VANILLA: if (!cancel_request)
         if(settings.noResultsText) {
            dropdown.html("<p>"+settings.noResultsText+"</p>");
            show_dropdown();
         }
      } else { // VANILLA
         cancel_request = false;
         hide_dropdown();
      }
   }

   // Highlight an item in the results dropdown
   function select_dropdown_item (item) {
      if(item) {
         if(selected_dropdown_item) {
            deselect_dropdown_item($(selected_dropdown_item));
         }

         item.addClass(settings.classes.selectedDropdownItem);
         selected_dropdown_item = item.get(0);
      }
   }

   // Remove highlighting from an item in the results dropdown
   function deselect_dropdown_item (item) {
      item.removeClass(settings.classes.selectedDropdownItem);
      selected_dropdown_item = null;
   }

   // Do a search and show the "searching" dropdown if the input is longer
   // than settings.minChars
   function do_search() {
      var query = input_box.val().toLowerCase();

      if(query && query.length) {
         if(selected_token) {
            deselect_token($(selected_token), POSITION.AFTER);
         }

         if(query.length >= settings.minChars) {
            show_dropdown_searching();
            clearTimeout(timeout);

            timeout = setTimeout(function(){
               run_search(query);
            }, settings.searchDelay);
         } else {
            hide_dropdown();
         }
      }
   }

   // Do the actual search
   function run_search(query) {
      var ajax_params = {};
      ajax_params.data = {};
      var url = computeURL();
      
      var cache_key = query + '-' + url;
      
      if (settings.dataFields) {
        $.each(settings.dataFields, function(index, value) {
          var el = $(value);
          if (!el) return;
          var fn = el.attr('name');
          if (!fn) return;
          var fnval = el.val();
          
          if (fn == 'CategoryID')
             cache_key = fnval + '-' + cache_key;
          ajax_params.data[fn] = fnval;
        });
      }
     
      // Check cache
      var cached_results = cache.get(cache_key);
      if (cached_results) {
         populate_dropdown(query, cached_results);
         return;
      }
      
      // Are we doing an ajax search or local data search?
      if (settings.url) {

        // Extract exisiting get params
        if (url.indexOf("?") > -1) {
           var parts = url.split("?");
           ajax_params.url = parts[0];

           var param_array = parts[1].split("&");
           $.each(param_array, function (index, value) {
              var kv = value.split("=");
              ajax_params.data[kv[0]] = kv[1];
           });
        } else {
          ajax_params.url = url;
        }

        // Prepare the request
        ajax_params.data[settings.queryParam] = query;
        ajax_params.type = settings.method;
        ajax_params.dataType = settings.contentType;
        if(settings.crossDomain) {
           ajax_params.dataType = "jsonp";
        }

        // Attach the success callback
        ajax_params.success = function(results) {
          if($.isFunction(settings.onResult)) {
            results = settings.onResult.call(hidden_input, results);
          }
          cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);

          // only populate the dropdown if the results are associated with the active search query
          if(input_box.val().toLowerCase() === query) {
            populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
          }
        };

        // Make the request
        $.ajax(ajax_params);
      } else if(settings.local_data) {
        // Do the search through local data
        var results = $.grep(settings.local_data, function (row) {
          return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
        });

        if ($.isFunction(settings.onResult)) {
          results = settings.onResult.call(hidden_input, results);
        }
        cache.add(cache_key, results);
        populate_dropdown(query, results);
      }
   }

   // compute the dynamic URL
   function computeURL() {
      var url = settings.url;
      if(typeof settings.url == 'function') {
         url = settings.url.call();
      }
      return url;
   }
};

// Really basic cache for the results
$.TokenList.Cache = function (options) {
   var settings = $.extend({
      max_size: 500
   }, options);

   var data = {};
   var size = 0;

   var flush = function () {
      data = {};
      size = 0;
   };

   this.add = function (query, results) {
      if(size > settings.max_size) {
         flush();
      }

      if(!data[query]) {
         size += 1;
      }

      data[query] = results;
   };

   this.get = function (query) {
      return data[query];
   };
};
}(jQuery));
